================================================================================
TEPS REGISTRATION CORE — API REFERENCE
================================================================================

Contributors: Mikal Farley
Created:      April 2026
Last Updated: April 11, 2026

Base URL:     http://<server-ip>:8585 (default port, configurable)
Content-Type: application/json (all request and response bodies)


================================================================================
CONVENTIONS
================================================================================

  - All responses are JSON unless otherwise noted.
  - Error responses use: {"error": "<message>"}
  - Registration fields accept both snake_case and camelCase in request bodies.
    Responses always use snake_case (via CodingKeys).
  - Boolean fields are true/false in JSON, stored as INTEGER 0/1 in SQLite.
  - Timestamps are ISO 8601: "2026-04-11T14:30:00Z"
  - IDs are integers (Int64).
  - The server must have an active event for most endpoints to work.
    If none exists, endpoints return 404 "No active event".


================================================================================
1. STATUS & CONFIGURATION
================================================================================


--- GET /api/status ----------------------------------------------------------

  Server health check and event summary.

  Response 200:
  {
    "status": "running",
    "version": "1.0",
    "port": 8585,
    "event_name": "Holiday Photos 2026",
    "has_event": true,
    "total_registered": 45,
    "total_checked_in": 12,
    "total_complete": 28,
    "schedule_date": "2026-04-11",       // only if schedule loaded
    "total_slots": 120                    // only if schedule loaded
  }


--- GET /api/event -----------------------------------------------------------

  Active event details.

  Response 200:
  {
    "id": 1,
    "name": "Holiday Photos 2026",
    "date": "2026-04-11",
    "api_source_url": null,
    "synced_at": null
  }

  Response 404: No active event.


--- POST /api/events ---------------------------------------------------------

  Create a new event (becomes the active event).

  Request body:
  {
    "name": "Holiday Photos 2026",     // required
    "date": "2026-04-11",              // optional, defaults to today
    "api_source_url": "https://..."    // optional
  }

  Response 201: Event object (same shape as GET /api/event).


--- GET /api/config/fields ---------------------------------------------------

  Registration form field configuration. Drives the dynamic form on
  /registration and /hoststation.

  Response 200:
  {
    "fields": [
      {
        "field": "name",
        "label": "Name",
        "enabled": true,
        "required": false,
        "firstAndLast": true          // only on "name" field
      },
      {
        "field": "email",
        "label": "Email Address",
        "enabled": true,
        "required": true
      },
      ...
    ],
    "allowDuplicates": true,
    "allowMultiple": true,
    "sendTextMessage": false,
    "sendEmail": false,
    "requireEmailOrPhone": false,
    "allowRequiredOverride": true,
    "nameRequired": false,
    "phoneCountry": "US",
    "registerButtonText": "REGISTER",
    "cancelButtonText": "",
    "optIns": [                        // the account's opt-in feed; an unconfigured
      {                                //   station has one required "terms" box
        "key": "terms",
        "label": "I agree to the terms of use and privacy policy",
        "required": true
      },
      {
        "key": "marketing",            // keys are tags: terms, marketing,
        "label": "Keep me updated",    //   account_<name>, portfolio_<name>
        "required": false
      }
    ],
    "customFields": [                  // only if configured
      {
        "field": "shirt_size",
        "label": "Shirt Size",
        "type": "select",
        "options": ["S", "M", "L", "XL"],
        "enabled": true,
        "required": false,
        "isCustom": true
      }
    ],
    "petTypeOptions": ["Cat", "Dog"],  // only if pet field configured
    "walkinCards": [                    // only if walk-in profile active
      {
        "id": "uuid",
        "title": "Contact Information",
        "subtitle": "Please provide your details",
        "fields": ["name", "email", "phone"],
        "buttonText": "Continue"
      }
    ]
  }


--- GET /api/config/statuses -------------------------------------------------

  Maps status strings to human-readable display names.

  Response 200:
  {
    "registered": "Waiting",
    "checked_in": "Checked In",
    "elf_processed": "Elf Done",
    "photographed": "Camera",
    "previewing": "Preview",
    "checkout": "Checkout",
    "complete": "Complete",
    "cancelled": "Cancelled",
    "no_show": "No Show"
  }


--- POST /api/validate/email -------------------------------------------------

  Server-authoritative email validation. Use this instead of client-side
  validation to ensure consistent rules.

  Request body:
  {
    "email": "user@example.com"
  }

  Response 200 (valid):
  {
    "valid": true
  }

  Response 200 (invalid):
  {
    "valid": false,
    "error": "Please enter a valid email address"
  }

  Rejects: numeric-only local parts, known disposable domains, trash patterns.


--- GET /api/schedule --------------------------------------------------------

  Today's time slots and reservations (from cloud sync or local schedule).

  Response 200:
  {
    "has_schedule": true,
    "schedule_date": "2026-04-11",
    "blocks": [
      {
        "block_id": 1,
        "service_id": 1,
        "service_name": "Photo Session",
        "start_time": "09:00:00",
        "end_time": "12:00:00",
        "label": "Photo Session",
        "block_state": "active"
      }
    ],
    "time_slots": [
      {
        "slot_id": 1,
        "service_id": 1,
        "slot_date": "2026-04-11",
        "start_time": "09:00:00",
        "end_time": "09:05:00",
        "capacity": 1,
        "booked_count": 1,
        "held_count": 0,
        "status": "available",
        "reservations": [
          {
            "confirmation_code": "RES-12345",
            "customer_first_name": "Jane",
            "customer_last_name": "Doe",
            "customer_email": "jane@example.com",
            "customer_phone": "5551234567",
            "party_size": 2,
            "status": "confirmed",
            "sync_id": "uuid-string",
            "package_id": 55,            // SERVER package id booked; null = none
            "booking_type": "vip",       // vip | corporate | standard; absent =
                                         // older server (nothing stored changes)
            "savings": [                 // saved online; null = the cloud
              {                          // sent none (older server),
                "source": "booking_offer", // [] = no saving
                "id": 2,
                "sku": "EB01",           // internal PLU; null = none set yet
                "label": "Early Bird Discount",
                "code": null,            // promo code, offer/coupon only
                "amount": -10            // ALWAYS negative
              }
            ],
            "extra_packages": [          // every package added online after
              {                          // the first; present when the cloud's
                "package_id": 55,        // envelope said
                "sku": "PKG-SG",         // extra_packages_payload_version 1
                "name": "Another Santa's Gift",
                "qty": 2,
                "unit_price": 20.0,      // paid online
                "line_total": 40.0,      // paid online
                "contents": [            // DELIVERED after the booking's item
                  {"product_id": 20, "qty": 4}  // limits; null = no product list
                ]
              }
            ],
            "added_packages": [          // the same packages as station pages
              {                          // show them — see below
                "name": "Another Santa's Gift",
                "qty": 2,
                "label": "Another Santa's Gift × 2",
                "includes": "4 × 5x7 Print"   // omitted = no product list
              }
            ]
          }
        ]
      }
    ],
    "station_cards": [               // the strip, one card each — see below
      {
        "id": "1:RES-12345",
        "kind": "customer",          // customer | open_seat
        "slot_id": 1,
        "service_id": 1,
        "start_time": "09:00:00",
        "end_time": "09:05:00",
        "reservation": { ... }       // that slot's reservation, exactly as
      },                             // in time_slots above (customer only)
      {
        "id": "2:open:1",
        "kind": "open_seat",
        "slot_id": 2,
        "service_id": 1,
        "start_time": "09:05:00",
        "end_time": "09:10:00",
        "seat": 1                    // 1-based among the slot's open seats
      }
    ]
  }

  `station_cards` is the schedule strip as the Mac draws it, built by the
  same function (ScheduleData.stationCards): one card per booked customer,
  with the time repeated, and one card per open seat — never two customers
  on one card, never one card for several open seats. Open seats are
  capacity − max(booked_count, number of reservations); party size takes no
  seats. Order: earliest time first, and at one time every customer before
  any open seat. An open seat's `slot_id` is the slot whose time a walk-in
  started from that card is given. `id` is stable while the slot and the
  booking are unchanged.
  Additive — `time_slots` is unchanged. Every response carries it (`[]`
  when there is no schedule).

  `savings` comes from the cloud schedule and sync pulls and is stored per
  booking (booking_savings). Each one is rung on the booking's order as a
  till-only line (see is_pos_line / saving_source below) — never taken off
  the total, which already had it off online.

  `extra_packages` (also on sync queue entries) is the cloud's record of every
  package the customer added to the booking online after the first one. It
  is stored per booking (booking_extra_packages) ONLY when the response
  carries `extra_packages_payload_version` (1 or later) — the envelope, not
  the row, says whether the cloud states them; then `[]` means none and
  clears what is stored. A response without it (an older admin) changes
  nothing. `package_id` and `product_id` are SERVER ids, matched through the
  catalog's cloud ids; `sku` is the package's own code — each package rings
  under this location's till code for it (the catalog's `sku`) when the
  catalog has it, else under this one.

  `added_packages` is attached here, on every Registration object and on
  every queue row whose booking holds some — whatever "Show color-coded
  packages and experiences" says. `includes` is what to fulfil: after the
  booking's item limits it can be less than the package usually holds.

  `package_id` / `booking_type` (also on sync queue entries) are stored on
  the registration the booking becomes, as `booked_package_cloud_id` and
  `booking_type` — both appear on every Registration object. They drive the
  station grid's package colour and VIP / corporate markers.

  `temporary_blocks` (with `temporary_blocks_payload_version`: 1) is passed
  through from the cloud schedule pull: the location's temporary blocks —
  new online bookings paused for a while — marked for the walk-up desk
  notice, running now or still to come, whatever date the schedule is for:
    [{ "id": 41, "service_id": null, "service_name": "",
       "starts_at": "2026-11-20T10:00:00Z", "ends_at": "2026-11-20T10:30:00Z",
       "next_session": { "slot_id": 7, "service_id": 3,
                         "start_time": "14:30:00",          // location clock
                         "starts_at": "2026-11-20T14:30:00Z" } }]
  `service_id` null = every service; `ends_at` null = no end; `next_session`
  null = no later session today has room. Present only when the cloud stated
  them (an older admin sends neither key).

  Response 200 (no schedule):
  {
    "has_schedule": false,
    "schedule_date": "",
    "blocks": [],
    "time_slots": [],
    "station_cards": []
  }


--- GET /api/schedule/walkin-notice -------------------------------------------

  The walk-up desk notice. The Host Station asks before every walk-in it
  starts (+ Walk-In, and a session's + Walk-In with ?slot_id=). It decides
  nothing — the page always offers "Proceed with walk-in", and opens the
  walk-in as before when this fails or is slow.

  Query parameters (optional):
    slot_id   the session the walk-in starts from; narrows the notice to
              blocks on that session's service (or every service)

  A block counts while starts_at <= now < ends_at, on this Mac's clock, so a
  block that ends on its own stops the notice without a pull. Overlapping
  blocks are judged by the one that ends last. Never shown in training mode.

  Response 200 (no block running):
  { "show": false }

  Response 200 (a block running):
  {
    "show": true,
    "title": "Temporary block period",
    "message": "Temporary block period — encourage new walk-ins to book online, or try again later.\n\nNext session with room: 2:30 PM",
    "try_later": true,                 // false: message ends "book online."
    "next_session": "2:30 PM"          // null when try_later is false
  }


================================================================================
2. REGISTRATION CRUD
================================================================================


--- GET /api/registrations ---------------------------------------------------

  List registrations for the active event. Supports filtering.

  Query parameters (all optional):
    ?status=checked_in        Filter by status
    ?station=camera           Filter by current station
    ?code=0411A3BF7K2P        Lookup by photo code (returns array of 0 or 1)
    ?date=2026-04-11          Filter by creation date

  Response 200: Array of Registration objects.
  [
    {
      "id": 1,
      "event_id": 1,
      "sync_id": "uuid",
      "source_type": 1,
      "code": "0411A3BF7K2P",
      "first_name": "Jane",
      "last_name": "Doe",
      "email": "jane@example.com",
      "phone": "5551234567",
      "child_name": "Alice;Bob",
      "child_wishlist": "Doll;Train",
      "child_age": "5;7",
      "elf_name": null,
      "postal_code": "90210",
      "street_address": null,
      "date_of_birth": null,
      "age_range": null,
      "group_size": 3,
      "optin": true,
      "optins": "{\"terms\": true, \"marketing\": true}",
      "notes": null,
      "order_text": null,
      "promo_code": null,
      "custom_fields": null,
      "pet_type": null,
      "pet_breed": null,
      "status": "checked_in",
      "current_station": "host",
      "reservation_time": "09:30:00",
      "is_late": false,
      "fastpass": false,
      "fastpass_status": null,
      "email_uploaded": false,
      "userdata_uploaded": false,
      "text_uploaded": false,
      "data_declined": false,
      "created_at": "2026-04-11T09:25:00Z",
      "updated_at": "2026-04-11T09:30:00Z",
      "status_display": "Checked In",
      "structured_notes": null,
      "is_queue_active": null
    }
  ]


--- POST /api/registrations -------------------------------------------------

  Create a new registration.

  Request body:
  {
    "first_name": "Jane",              // or "firstName" or "name" (auto-split)
    "last_name": "Doe",                // or "lastName"
    "email": "jane@example.com",       // validated server-side
    "phone": "(555) 123-4567",         // auto-normalized to digits
    "child_name": "Alice;Bob",         // semicolon-delimited for multi-child
    "child_wishlist": "Doll;Train",
    "child_age": "5;7",
    "postal_code": "90210",
    "street_address": "123 Main St",
    "date_of_birth": "1990-01-15",
    "age_range": "25-34",
    "group_size": 3,
    "optin": true,                     // ignored when optins carry answers
    "optins": {"terms": true, "marketing": true},  // object or JSON string
    "notes": "VIP customer",
    "order_text": "Premium package",
    "promo_code": "HOLIDAY20",
    "custom_fields": {"shirt_size": "M"},          // object or JSON string
    "pet_type": "Dog",
    "pet_breed": "Golden Retriever",
    "reservation_time": "09:30:00",
    "fastpass": false,
    "data_declined": false,
    "source_type": 1                   // 0=reservation, 1=walkin_local, 2=walkin_web
  }

  All fields except first_name are optional.

  Response 201: Full Registration object (includes generated code, sync_id).

  Response 400: Validation error.
  {
    "error": "Please enter a valid email address",
    "field": "email"
  }

  Response 409: Duplicate detected (when allowDuplicates is off).
  {
    "error": "A registration with this email already exists",
    "field": "email"
  }

  Notes:
    - Photo code is auto-generated from RegistrationConfig.photoCodeSegments.
    - Phone is stripped to digits only.
    - Email is validated against the same rules as POST /api/validate/email.
    - The "name" field is auto-split on first space into first_name/last_name.
    - "optin" is stored as the "marketing" answer whenever "optins" carries
      answers (a set without a "marketing" box is false); the client's own
      "optin" counts only when it sends no answers.


--- GET /api/registrations/{id} ----------------------------------------------

  Get a single registration by ID.

  Response 200: Registration object.
  Response 404: Not found.


--- PUT /api/registrations/{id} ----------------------------------------------

  Update fields on a registration. Only include fields you want to change.

  Request body (partial update):
  {
    "first_name": "Janet",
    "elf_name": "Sparkle",
    "notes": "wishlist: Bicycle\nelf: Sparkle"
  }

  Accepts both snake_case and camelCase keys. Auto-maps:
    firstName -> first_name, childName -> child_name, etc.

  Phone is re-normalized. Custom fields and optins are re-serialized if
  provided as objects. When optins are sent, "optin" is set from their
  "marketing" answer, whatever "optin" the client sent — send the whole
  answer set, not only the box that changed.

  Response 200: Updated Registration object.
  Response 404: Not found.


--- GET /api/registrations/code/{code} ---------------------------------------

  Lookup a registration by photo code. Returns the registration plus
  TEPS-REG-* response headers for inter-station data transfer.

  Response 200: Registration object (full customer data is in the JSON body).
  Response headers (non-PII operational only — see section 9):
    TEPS-REG-CODE: 0411A3BF7K2P
    TEPS-REG-FASTPASS: FALSE
    TEPS-REG-FASTPASSSTATUS:
    TEPS-REG-TOTAL: 49.99           // if order exists
    TEPS-REG-DISCOUNT: 10.00        // if order exists
    TEPS-REG-PACKAGES: 8x10,29.99,PKG1;Keychain,9.99,ADD1  // if packages

  Response 404: Not found.


================================================================================
3. WORKFLOW
================================================================================

  All workflow endpoints return the updated Registration object on success.


--- POST /api/registrations/{id}/checkin -------------------------------------

  Move from "registered" to "checked_in". Enters the host station queue.

  Precondition: status must be "registered".

  Side effects:
    - Status -> "checked_in"
    - current_station -> "host"
    - Queue log: "entered" at "host"

  Response 200: Updated Registration.
  Response 400: Wrong status.


--- POST /api/registrations/{id}/start ---------------------------------------

  Activate a waiting customer at their current station. The operator clicks
  "Start" to begin serving this customer.

  Precondition: must have a current_station.

  Side effects:
    - Queue log: "started" at current station
    - No status change (status reflects which station, not active/waiting)

  Response 200: Updated Registration.
  Response 400: Not at a station.

  Note: The "is_queue_active" enriched field will become true for this
  registration in subsequent GET /api/queue/{station} responses.


--- POST /api/registrations/{id}/advance -------------------------------------

  Move to the next station in the workflow, or mark complete if at the
  last station.

  Optional request body (fields to update during advance):
  {
    "elf_name": "Jingle",
    "child_wishlist": "Bicycle;Doll",
    "notes": "wishlist: Bicycle\nelf: Jingle"
  }

  Side effects:
    - Queue log: "exited" at current station
    - If next station exists:
        Status -> station-appropriate value
        current_station -> next station
        Queue log: "entered" at next station
    - If no next station (checkout is last):
        Status -> "complete"
        current_station -> null

  Station -> status mapping:
    host     -> "checked_in"
    elf      -> "elf_processed"
    camera   -> "photographed"
    preview  -> "previewing"
    checkout -> "checkout"
    (after checkout -> "complete")

  Response 200: Updated Registration + TEPS-REG-* headers.
  Response 400: Not at a station.


--- POST /api/registrations/{id}/return --------------------------------------

  Send back to the previous station.

  Precondition: must have a previous station (can't return from host).

  Side effects:
    - Queue log: "exited" at current station
    - Status -> previous station's status
    - current_station -> previous station
    - Queue log: "entered" at previous station

  Response 200: Updated Registration.
  Response 400: No previous station.


--- POST /api/registrations/{id}/cancel --------------------------------------

  Cancel a registration at any stage.

  Side effects:
    - Status -> "cancelled"
    - current_station -> null

  Response 200: Updated Registration.


================================================================================
4. QUEUE & OVERVIEW
================================================================================


--- GET /api/queue/{station_type} --------------------------------------------

  Get the queue for a specific station. Returns registrations currently at
  that station, ordered by active first, then by arrival time.

  station_type: host, elf, camera, preview, checkout

  Response 200: Array of Registration objects.
  Each includes "is_queue_active": true/false indicating whether the operator
  has started this customer.


--- GET /api/queue/{station_type}/current ------------------------------------

  Get the currently active (started) customer at a station. Used by the
  Santa display to show who's being photographed.

  For station_type "santa", checks both "camera" and "host" stations.

  Response 200: Registration object + TEPS-REG-* headers.
  Response 200 (no active customer): {"message": "No active customer"}


--- GET /api/overview --------------------------------------------------------

  Aggregate station statistics for the overview dashboard.

  Response 200:
  {
    "event_name": "Holiday Photos 2026",
    "total_registered": 45,
    "total_checked_in": 12,
    "total_complete": 28,
    "total_waiting": 8,
    "total_in_progress": 5,
    "stations": [
      {"type": "host", "name": "Host Station", "count": 3},
      {"type": "camera", "name": "Camera Station", "count": 2},
      {"type": "preview", "name": "Preview Station", "count": 1},
      {"type": "checkout", "name": "Checkout Station", "count": 2}
    ]
  }


--- GET /api/registrations/{id}/queuelog -------------------------------------

  Get the full queue log history for a registration.

  Response 200: Array of QueueLogEntry objects.
  [
    {
      "id": 1,
      "registration_id": 5,
      "sync_id": "uuid",
      "station_type": "host",
      "action": "entered",
      "actor_station_id": null,
      "timestamp": "2026-04-11T09:30:00Z"
    }
  ]


================================================================================
5. ORDERS & PACKAGES
================================================================================


--- POST /api/registrations/{id}/order ---------------------------------------

  Create an order for a registration. If one already exists, returns the
  existing order (idempotent).

  Request body (optional):
  {
    "promo_code": "HOLIDAY20",
    "notes": "Gift wrap requested",
    "fastpass": false
  }

  Response 201: Order object.
  Response 200: Existing Order (if already created).

  A booked customer's added packages, then their online savings, are added
  to a new order as till-only lines ("extra_package_source": "booking", then
  "saving_source": "booking") — the order total is not changed. Each copy of
  an added package is its own line at the price paid online for one.


--- GET /api/registrations/{id}/order ----------------------------------------

  Get the order for a registration.

  Response 200: Order object.
  {
    "id": 1,
    "registration_id": 5,
    "order_status": 0,
    "santa_slap": 0,
    "subtotal": 0,
    "tax": 0,
    "total": 0,
    "discount": 0,
    "promo_code": "HOLIDAY20",
    "notes": "Gift wrap requested",
    "order_text": null,
    "custom_fields": null,
    "fastpass": false,
    "fastpass_status": null,
    "last_action": null,
    "quantity": 1,
    "created_at": "2026-04-11T10:00:00Z",
    "updated_at": "2026-04-11T10:00:00Z",
    "packages": null
  }

  Response 404: No order found.


--- POST /api/orders/{id}/status ---------------------------------------------

  Update order status (and optionally santa_slap count).

  Request body:
  {
    "status": 3,              // 0=new, 1=elf, 2=photo, 3=POS, 9=cancelled
    "santa_slap": 2           // optional
  }

  Response 200: {"success": true}


--- POST /api/orders/{id}/packages -------------------------------------------

  Add a package line item to an order.

  Request body:
  {
    "title": "8x10 Print",    // required
    "code": "PKG-8X10",       // optional
    "price": 29.99,            // optional, default 0
    "is_addon": false          // optional, default false
  }

  Response 201: OrderPackage object.
  {
    "id": 1,
    "order_id": 1,
    "title": "8x10 Print",
    "code": "PKG-8X10",
    "price": 29.99,
    "is_addon": false,
    "quantity": 1,
    "is_pos_line": false,      // true = till-only line a package emitted;
                               // never part of the order total
    "saving_source": "booking",// present only on a till-only line carrying a
                               // saving the customer got online (negative
                               // price, code = its PLU); omitted otherwise
    "extra_package_source": "booking" // present only on a till-only line
                               // carrying a package added to the booking
                               // online and already paid for (price = paid
                               // per copy, code = its till code); omitted
                               // otherwise
  }


--- GET /api/orders/{id}/packages --------------------------------------------

  List all packages on an order.

  Response 200: Array of OrderPackage objects.


================================================================================
6. UPLOADS & CODE CHANGE
================================================================================


--- POST /api/registrations/{id}/userdata ------------------------------------

  Upload customer data to the cloud (phototouchinc.com). Marks the
  registration's userdata_uploaded flag on success.

  Response 200:
  {
    "success": true,
    "response": "<server response body>"
  }

  Response 500:
  {
    "success": false,
    "error": "Upload failed: <reason>"
  }


--- POST /api/code/reassign -------------------------------------------------

  Reassign a photo code (rename the sitting folder).

  Request body:
  {
    "registration_id": 5,
    "new_code": "NEWCODE123",
    "filenames": ["IMG001.jpg", "IMG002.jpg"]  // optional, auto-detected
  }

  Checks that new_code is not already in use (409 if so).
  Updates the code in the database after renaming files.

  Response 200: ReassignResult object.
  Response 409: Code already in use.


--- POST /api/code/swap ------------------------------------------------------

  Swap codes between two registrations (swap their sitting folders).

  Request body:
  {
    "code_a": "CODE111",
    "code_b": "CODE222"
  }

  Response 200: SwapResult object.


--- GET /api/code/files/{code} -----------------------------------------------

  List files in the sitting folder for a photo code.

  Response 200: Array of file info objects.


================================================================================
7. THEME
================================================================================


--- GET /api/theme/css -------------------------------------------------------

  Returns CSS variables for the active theme.

  Response 200 (text/css):
    :root {
      --theme-bg: #1a1a2e;
      --theme-text: #ffffff;
      --theme-heading: #ffffff;
      --theme-accent: #e94560;
      ...
    }


--- GET /api/theme/background ------------------------------------------------

  Returns the custom background image if one is set.

  Response 200: Image data (image/png, image/jpeg, or image/webp).
  Response 404: No custom background set.


--- GET /api/theme/logo ------------------------------------------------------

  Returns the custom logo image if one is set.

  Response 200: Image data (image/png, image/jpeg, image/svg+xml, or image/webp).
  Response 404: No custom logo set.


================================================================================
8. TESTING
================================================================================


--- POST /api/test -----------------------------------------------------------

  Create a test registration for development/debugging.
  Code is "__TEST__", name is "Test Registration".

  Response 201: Registration object.


--- POST /api/test/cleanup ---------------------------------------------------

  Delete all test registrations (code = "__TEST__").

  Response 200:
  {
    "deleted": 3
  }


================================================================================
9. TEPS-REG RESPONSE HEADERS
================================================================================

  Certain endpoints include custom response headers for inter-station data
  transfer. Camera software and other station tools read these headers to
  get operational info without parsing JSON.

  SECURITY (TEPSTX-291): These headers no longer carry customer PII. The
  fields removed below leaked personal data into inter-station HTTP responses.
  The COMPLETE registration (including name, email, child name, etc.) is still
  returned in the JSON response body, so any client that needs personal data
  MUST read the body. Only non-PII operational fields remain in the headers.

  Header                    Value
  ----------------------    -----------------------------------------
  TEPS-REG-CODE             Photo code (non-PII correlation key)
  TEPS-REG-FASTPASS         "TRUE" or "FALSE"
  TEPS-REG-FASTPASSSTATUS   Fastpass status string
  TEPS-REG-TOTAL            Order total (e.g. "49.99")
  TEPS-REG-DISCOUNT         Order discount (e.g. "10.00")
  TEPS-REG-PACKAGES         Semicolon-separated: "Title,Price,Code;..."

  REMOVED (read from the JSON body instead):
    TEPS-REG-NAME, TEPS-REG-LASTNAME, TEPS-REG-EMAIL, TEPS-REG-CHILDNAME,
    TEPS-REG-PROMOCODE, TEPS-REG-ORDERTEXT

  Included on:
    - GET /api/registrations/code/{code}
    - POST /api/registrations/{id}/advance
    - GET /api/queue/{station_type}/current


================================================================================
END OF DOCUMENT
================================================================================
